﻿using Renci.SshNet;
using System;
using System.IO;
using System.Text;

namespace CommonUtils
{
    /// <summary>
    /// 切不了用户,始终是登录用户
    /// 切不了目录,始终是home目录
    /// </summary>
    public static class SshUtil
    {
        const double timeoutSeconds = 5.3;
        public const int defaultPort = 22;

        static Map<SshClient, string> Passwords = new Map<SshClient, string>();
        static Map<SshClient, string> MachineNames = new Map<SshClient, string>();
        static Map<SshClient, double> TimeoutSeconds = new Map<SshClient, double>();

        public static SshClient CreateClient(string host, int port, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        {
            LogUtil.log("init ssh@" + host);
            var client = new SshClient(host, port, user, pwd);
            client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
            Passwords[client] = pwd;
            TimeoutSeconds[client] = timeoutSeconds;
            return client;
        }

        public static SshClient CreateClient(string host, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        => CreateClient(host, defaultPort, user, pwd, timeoutSeconds);

        public static SshClient NewClient(string host, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        => CreateClient(host, user, pwd, timeoutSeconds);

        public static SshClient NewClient(string host, int port, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        => CreateClient(host, port, user, pwd, timeoutSeconds);

        public static SshClient NewConnect(string host, int port, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        => CreateClient(host, port, user, pwd, timeoutSeconds);

        public static void Open(this SshClient client)
        => client.Connect();

        /// <summary>
        /// 不会失败,可以放进finally
        /// </summary>
        /// <param name="client"></param>
        public static void Close(this SshClient client)
        => client.Disconnect();

        public static string ResultAndError(this SshCommand command)
        => command.Result + command.Error;

        public static string Host(this SshClient client)
        => client.ConnectionInfo.Host;

        public static int Port(this SshClient client)
        => client.ConnectionInfo.Port;

        public static string User(this SshClient client)
        => client.ConnectionInfo.Username;

        public static bool IsRoot(this SshClient client)
        => client.User() == "root";

        public static string OperateMark(this SshClient client)
        => client.IsRoot() ? "#" : "$";

        public static string Password(this SshClient client)
        => Passwords[client];

        public static double TimeoutSecond(this SshClient client)
        => TimeoutSeconds[client];

        public static string MachineName(this SshClient client)
        {
            if (!MachineNames.ContainsKey(client))
                MachineNames[client] = client.RunCommand("uname -n").Result.Trim();
            return MachineNames[client];
        }

        public static bool FileExits(this SshClient client, string path)
        {
            var cmd = client.RunCommand("ls " + path);
            var info = (cmd.Result + cmd.Error).Trim();
            return !StringUtil.Contains(info, "没有那个文件或目录", "cannot access", "No such file or directory");
        }

        public static string WorkFolder(this SshClient client)
        => client.RunCommand("pwd").Result.Trim();

        public static string HeadInfo(this SshClient client)
        => StringUtil.Format("[{}@{} ~]{} ", client.User(), client.MachineName(), client.OperateMark());

        public static string ExecuteCommand(this SshClient client, string cmd, Encoding encoding = null)
        {
            var log = client.HeadInfo() + cmd;
            //var commander = client.RunCommand(cmd);//这个不能指定编码
            var commander = client.CreateCommand(cmd, encoding.Null2utf8());//这个可以指定编码
            commander.Execute();
            var result = (commander.Result + commander.Error).Trim();
            if (StringUtil.IsNotEmpty(result))
                log += "\r\n" + result;
            LogUtil.log(log);
            return result;
        }

        public static string Command(this SshClient client, string cmd, Encoding encoding = null)
        => client.ExecuteCommand(cmd, encoding);

        public static string SuCommand(this SshClient client, string cmd, params object[] args)
        {
            cmd = StringUtil.Format(cmd, args);
            if (client.IsRoot())
                return client.ExecuteCommand(cmd);
            else
                return client.ExecuteCommand("echo \"" + client.Password() + "\" | sudo -S " + cmd);
        }

        public static void FileDelete(this SshClient client, string path)
        {
            LogUtil.log("删除: " + path);
            client.Command("rm " + path);
        }

        public static void Grep(this SshClient client, string keyword)
        => client.Command("ps -ef | grep " + keyword);

        public static void GrepJava(this SshClient client)
        => client.Grep("java");

        public static void Chmod(this SshClient client, string target)
        => client.SuCommand("chmod 777 " + target);

        public static void Makedir(this SshClient client, string target)
        {
            if (client.FileExits(target))
                return;
            client.SuCommand("mkdir -p " + target);
            client.Chmod(target);
        }

        public static void MakeFileDir(this SshClient client, string target)
        => client.Makedir(target.Substring(0, target.LastIndexOf('/')));

        public static void Unzip(this SshClient client, string zip, string target, Encoding encoding = null)
        //这里unzip打印出的?????可以忽略,用ll命令(utf-8)是正常的中文
        => client.Command("unzip -O " + encoding.Null2utf8().WebName + " -o " + zip + " -d " + target, encoding);

        public static void UploadFolder(this SshClient client, string folder, string target, Encoding encoding = null)
        {
            client.Makedir(target);

            var zip = ZipUtil.Create(folder, false, encoding);
            var sftp = new SftpClient(client.Host(), client.Port(), client.User(), client.Password());
            sftp.Connect();
            LogUtil.log(StringUtil.Format("[{}@sftp]: replace uploading {} to {}", client.User(), folder, target));
            var targetZip = target + ".zip";
            LogUtil.log(StringUtil.Format("[{}@sftp]: uploading {} to {}", client.User(), folder + "(.zip)", targetZip));
            sftp.UploadFile(zip, targetZip);
            sftp.Disconnect();
            client.Unzip(targetZip, target, encoding);
            LogUtil.log(StringUtil.Format("[{}@sftp]: upload success!", client.User()));

            return;

            //兼容性不好,有时会卡在uploading
            var scp = new ScpClient(client.Host(), client.Port(), client.User(), client.Password());
            scp.Connect();
            LogUtil.log(StringUtil.Format("[{}@scp]: uploading {} to {}", client.User(), folder, target));
            scp.Upload(new DirectoryInfo(folder), target);
            scp.Disconnect();
            LogUtil.log(StringUtil.Format("[{}@scp]: upload success!", client.User()));
        }

        public static void UploadFile(this SshClient client, string file, string target)
        {
            client.MakeFileDir(target);

            var sftp = new SftpClient(client.Host(), client.Port(), client.User(), client.Password());
            sftp.Connect();
            LogUtil.log(StringUtil.Format("[{}@sftp]: uploading {} to {}", client.User(), file, target));
            sftp.UploadFile(new FileInfo(file).OpenRead(), target);
            sftp.Disconnect();
            LogUtil.log(StringUtil.Format("[{}@sftp]: upload success!", client.User()));

            return;

            //兼容性不好,有时会卡在uploading
            var scp = new ScpClient(client.Host(), client.Port(), client.User(), client.Password());
            scp.Connect();
            LogUtil.log(StringUtil.Format("[{}@scp]: uploading {} to {}", client.User(), file, target));
            scp.Upload(new FileInfo(file), target);
            scp.Disconnect();
            LogUtil.log(StringUtil.Format("[{}@scp]: upload success!", client.User()));
        }

        public static string GetIpConfig(this SshClient client)
        => client.ExecuteCommand("ifconfig");

        public static void PrintIpConfig(this SshClient client)
        => client.GetIpConfig().Print();

        /// <summary>
        /// 不准确,有些客户端是先连接后验证的,所以TryConnect更准确
        /// </summary>
        public static bool CanConnect(this SshClient client)
        {
            try
            {
                client.Open();
                client.Close();
                return true;
            }
            catch
            {
                return false;
            }
        }

        public static SshCommandState TryCommand(this SshClient client)
        {
            LogUtil.PrintInfo("{0}:{1} is trying to ssh {2} limt {3} s ...",
                         client.User(), client.Password(), client.Host(), client.TimeoutSecond());
            try
            {
                client.Open();
                var machineName = client.MachineName();
                if (machineName.IsNotEmpty() && machineName.NotEquals("Please login:"))
                {
                    LogUtil.PrintInfo("SshClient connectted success! 获取到主机名: " + machineName);
                    return SshCommandState.Success;
                }
                else
                {
                    LogUtil.PrintInfo("SshClient connectted fail!");
                    return SshCommandState.Fail;
                }
            }
            catch (Exception ex)
            {
                LogUtil.PrintInfo("Exception: " + ex.Message);

                var exName = ex.GetType().Name;
                switch (exName)
                {
                    case "SshOperationTimeoutException":
                        return SshCommandState.Timeout;

                    case "SshAuthenticationException":
                        return SshCommandState.AuthenticationDeny;

                    case "SocketException":
                        return SshCommandState.ProtocalDeny;

                    case "SshConnectionException":
                        return SshCommandState.ConnectionException;

                    default:
                        ex.HelpLink = client.ConnectionInfo.Host;
                        LogUtil.Record(ex);
                        return SshCommandState.Unknown;
                }
            }
            finally
            {
                client.Close();
            }
        }

        public static SshCommandState TryCommand(string host, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        {
            var client = CreateClient(host, user, pwd, timeoutSeconds);
            return client.TryCommand();
        }

        public static bool RealCanConnect(this SshClient client)
        => client.TryCommand() == SshCommandState.Success;

        public static bool RealCanConnect(string host, string user, string pwd, double timeoutSeconds = timeoutSeconds)
        => TryCommand(host, user, pwd, timeoutSeconds) == SshCommandState.Success;

        static int count;

        public static void Test(this SshClient client, Encoding encoding = null)
        {
            LogUtil.log("查看ip命令");
            client.Command("ip addr");

            LogUtil.log("故意输错命令 提示 未找到命令 查看是中文还是英文");
            client.Command("ipaddr");

            LogUtil.log("创建中文文件夹《测试》，ls - l 命令查看能否正常显示");
            client.Makedir("/test/测试");
            client.Command("ls -l /test/测试");

            LogUtil.log("todo 压缩包能否正常解压，名字是否显示正常,内容是否正常，可否被覆盖");
            client.UploadFolder("D:\\Subversion\\MyPrograms\\Zip测试", "/test/Zip测试" + ++count, encoding);
            client.Command("ls -l /test", encoding);
            client.Command("ls -l /test/Zip测试1", encoding);
        }

        public static void Test(string ip, string user, string pwd)
        {
            var ssh = NewClient(ip, user, pwd);
            ssh.Open();
            ssh.Test();
            ssh.Test(Encodings.UTF8);
            ssh.Test(Encodings.Default);
            ssh.Test(Encodings.ASCII);
            ssh.Test(Encodings.GB2312);
            ssh.Close();
        }

        public static void Test(string ip, int port, string user, string pwd)
        {
            var ssh = NewClient(ip, port, user, pwd);
            ssh.Open();
            ssh.Test();
            ssh.Test(Encodings.UTF8);
            ssh.Test(Encodings.Default);
            ssh.Test(Encodings.ASCII);
            ssh.Test(Encodings.GB2312);
            ssh.Close();
        }

        public static bool CanPing(this SshClient client)
        => NetworkUtil.CanPing(client.Host());

        public static bool CanTelnet(this SshClient client)
        => NetworkUtil.CanTelnet(client.Host(), client.Port());
    }
}
